package dd.lo.practice;

import dd.lo.util.ImageUtils;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class DftDisplay extends Application {

    public static void start(String[] args) {
        launch(args);
    }

    private static final int DEFAULT_WIDTH = 1200;
    private static final int DEFAULT_HEIGHT = 900;

    private Mat srcImg;

    private ImageView imageView;

    private Text errMsgText;

    private void imageProcess() {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                //【0】对原始图像做一些通道和类型的限制
                if (1 < srcImg.channels()) {
                    Imgproc.cvtColor(srcImg, srcImg, Imgproc.COLOR_BGR2GRAY);
                }
                if (CvType.CV_32F != srcImg.type()) {
                    srcImg.assignTo(srcImg, CvType.CV_32F);
                }
                //【1】将输入图像延扩（下和右）到最佳尺寸，边界用0补充，最佳尺寸高宽为m、n
                int m = Core.getOptimalDFTSize(srcImg.rows());
                int n = Core.getOptimalDFTSize(srcImg.cols());
                Mat padded = new Mat();
                Core.copyMakeBorder(srcImg, padded, 0, m - srcImg.height(), 0, n - srcImg.width(), Core.BORDER_CONSTANT, Scalar.all(0));
                //【2】为傅里叶变换的结果（结果类型为复数，包含实部和虚部）分配存储空间，分配一个padded矩阵大小的平面存储实部，另外分配一个一样大小的平面存储虚部，并将两个通道合并在一起变成一个多通道的矩阵
                List<Mat> planes = new ArrayList<>();
                planes.add(padded);
                planes.add(Mat.zeros(padded.size(), CvType.CV_32F));
                Mat complexImg = new Mat();
                Core.merge(planes, complexImg);
                //【3】就地计算模式进行离散傅里叶变换
                Core.dft(complexImg, complexImg);
                //【4】将复数转换为幅值，Re实数部分，Im虚数部分，即magnitude函数：M = sqrt( Re(DFT(I))^2 + Im(DFT(I))^2 )
                Core.split(complexImg, planes); //planes[0] = Re(DFT(I)), planes[1] = Im(DFT(I))
                Mat magnitudeImg = planes.get(0);
                Core.magnitude(planes.get(0), planes.get(1), magnitudeImg); //将幅值计算结果放在planes[0]中
                //【5】进行对数尺度缩放，傅里叶变换的结果幅度范围大到不适合在屏幕上显示，为了凸显在屏幕上显示的连续性，我们用对数尺度来替换线性尺度
                //对数替换的方式：M1 = log(1 + M)
                Core.add(magnitudeImg, Mat.ones(magnitudeImg.size(), CvType.CV_32F), magnitudeImg);
                Core.log(magnitudeImg, magnitudeImg);
                //【6】剪切和重分布幅度图的象限，傅里叶变换之后，结果图的四个角均指向原点，并且将幅度图从中间划开成四个部分，每个部分是幅度图的一个象限，分割旋转每一个象限并将原点合并在一起，方便显示
                magnitudeImg = new Mat(magnitudeImg, new Range(0, magnitudeImg.rows() & -2), new Range(0, magnitudeImg.cols() & -2)); //"& -2"的作用是高或宽如果是计数的话就剪裁掉奇数的频谱
                int cx = magnitudeImg.cols() / 2;
                int cy = magnitudeImg.rows() / 2;
                Mat q0 = new Mat(magnitudeImg, new Rect(0, 0, cx, cy)); //ROI区域的左上
                Mat q1 = new Mat(magnitudeImg, new Rect(cx, 0, cx, cy)); //ROI区域的右上
                Mat q2 = new Mat(magnitudeImg, new Rect(0, cy, cx, cy)); //ROI区域的左下
                Mat q3 = new Mat(magnitudeImg, new Rect(cx, cy, cx, cy)); //ROI区域的右下
                //交换象限，左上与右下交换
                Mat tmp = new Mat();
                q0.copyTo(tmp);
                q3.copyTo(q0);
                tmp.copyTo(q3);
                //交换象限，右上与左下交换
                q1.copyTo(tmp);
                q2.copyTo(q1);
                tmp.copyTo(q2);
                //【7】归一化，这一步仍然是为了显示，由于幅值超过了[0, 255]的显示范围，因此需要将幅值全部归一化到[0, 255]范围以利于显示
                Core.normalize(magnitudeImg, magnitudeImg, 0.0, 255.0, Core.NORM_MINMAX);
                //【8】显示
                ImageUtils.fitImgSize(magnitudeImg, DEFAULT_WIDTH, DEFAULT_HEIGHT);
                MatOfByte buffer = new MatOfByte();
                Imgcodecs.imencode(".jpeg", magnitudeImg, buffer);
                Platform.runLater(() -> {
                    imageView.setImage(new Image(new ByteArrayInputStream(buffer.toArray())));
                    errMsgText.setText("");
                });
                return null;
            }
        };
        new Thread(task).start();
    }

    @Override
    public void start(Stage stage) throws Exception {
        Button imgChooserBtn = new Button("选择图片");
        imgChooserBtn.setOnMouseClicked(e -> {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("选择图片");
            fileChooser.setInitialDirectory(new File(System.getProperty("user.home") + "/Downloads")
            );
            fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("All Images", "*.*"));
            File imgFile = fileChooser.showOpenDialog(stage);
            if (null == imgFile) return;
            //灰度模式读取一张图片，转换功能只支持单个通道的图片，故用灰度模式
            srcImg = Imgcodecs.imread(imgFile.getAbsolutePath(), Imgcodecs.IMREAD_GRAYSCALE);
            if (srcImg.empty()) Platform.runLater(() -> errMsgText.setText("图片读取失败"));
            imageProcess();
        });
        errMsgText = new Text();
        errMsgText.setFill(Color.RED);
        Button exitBtn = new Button("关闭");
        exitBtn.setOnMouseClicked(e -> {
            Platform.exit();
        });
        FlowPane bottomControl = new FlowPane();
        bottomControl.getChildren().addAll(imgChooserBtn, errMsgText, exitBtn);
        bottomControl.setPrefHeight(50);
        BorderPane root = new BorderPane();
        imageView = new ImageView(); //imageView放到一个pane的中间
        root.setCenter(imageView);
        root.setBottom(bottomControl);
        Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT + 50);
        stage.setScene(scene);
        stage.setTitle("图片傅里叶转换结果显示");
        stage.setResizable(false);
        stage.show();
    }
}
